76. 最小覆盖子串

76. 最小覆盖子串

题目链接
注意,这个题是困难,以后困难题少花时间

分析

首先想到的,还是快慢指针这个框架,移动快慢指针的方式也很简单,

参考 哈希表 就会明白:将一个字符的 ASCII 码当作下标,实际上就是一种哈希函数,key 就是字符,value 就是字符出现的次数

这个其实给了我们启发,所有对字符串包含的考察,我们都可以使用这个方法。

解题

这个题其实我一上来就有思路了,但是写的过程非常的难受,老是报错
有几点需要注意:
之前我都是习惯使用,[slow,fast) 例如 209. 长度最小的子数组,但是这个题不是,这个题用的是左闭右开区间 [slow,fast),因此会出现一个极端情况,就是当遍历完数组的时候,fast==source.length,此时快指针实际上是超长了的,但是此时如果还可以收缩满指针,那么就不能直接退出,因此我们在 for 循环中不能使用 fast<source.length,这也是为什么官方题解都使用两层循环的原因,用两层循环的好处就是移动慢指针的时候不用考虑快指针的情况。

public String minWindow(String s, String t) {
    char[] targetCharArr = t.toCharArray();
    char[] sourceCharArr = s.toCharArray();
    int[] targetCount = new int[128];
    int[] sourceCount = new int[128];
    // 记录目标字符串中的字符出现次数
    for(int i=0;i<targetCharArr.length;i++){
        int charObj = targetCharArr[i];
        targetCount[charObj]++;
    }
    int count=0;
    // 子数组的不包含 fast,区间为[slow,fast)
    int slow=0,fast=0;
    int minLength = sourceCharArr.length+1;
    int startPoint = 0;
    for(;;){
        // 根据count判断移动快指针还是慢指针
        if(count<targetCharArr.length){
	        // 需要判断指针是否超出范围
            if(fast==sourceCharArr.length ){
                break;
            }
            int charFast = sourceCharArr[fast];
            // 不在目标字符串中出现的字符不统计
            if(targetCount[charFast]>0){
                sourceCount[charFast]+=1;
                // 如果子数组中,某个字符串的数量已经大于这个字符串在目标字符串中的数量,那count就没有再累加的必要了
                if(sourceCount[charFast]<=targetCount[charFast]){
                    count++;
                }    
            }
            fast++;
        }else{
            // 计算长度
            int nowLength = fast-slow;
            if(nowLength < minLength){
	            // 记录长度的同时记录起点,方便最后返回字符串
                minLength=nowLength;
                startPoint = slow;
            }
            // 需要判断指针是否超出范围
            if(slow==sourceCharArr.length ){
                break;
            }
            int charSlow = sourceCharArr[slow];
            // 不在目标字符串中出现的字符不统计
            if(targetCount[charSlow]>0){
                sourceCount[charSlow]-=1;
                // 字符在子数组中的数量小于在目标字符串中的数量,才减少count
                if(sourceCount[charSlow]<targetCount[charSlow]){
                    count--;
                }
            }
            slow++;
        }
    }
    if(minLength == sourceCharArr.length+1){
        return "";
    }
    return s.substring(startPoint,startPoint+minLength);

}

附赠官方题解

/**  
 * 优化思路:  
 * count 表示 字符在s中出现的频次  
 * count这个概念的引入成功实现了O(1)的时间按复杂度判断窗口内的元素是否包含t中所有的元素,对于需要保证元素出现次数的题目,都可以用count这个概念  
 * 此外,其实在 s 中,有的字符我们是不关心的,我们只关心 t 中出现的字符,在遍历过程中,我们完全可以跳过没在 t 中出现过的字符,避免没必要的对比  
 * 用来了ASCII码跟数字的对应关系,直接用一个长度为128(因为ASC代码总共128位) 的 int数组存放字符出现次数,省去了使用HashMap的内存和复杂度,  
 */public static String minWindow1(String s, String t) {  
    if (s.equals("") || s.length() == 0 || t .equals("") || t.length() == 0 || s.length() < t.length()) {  
        return "";  
    }  
    //题目中说 s 和 t 由英文字母组成 ,也就是说都是ASC代码,可以直接使用数字表示,那就直接当成数组下标,省去了使用HashMap的内存和复杂度  
    //数组存放指定字符出现的次数 数组长度为128,因为ASC代码总共128位  
    int[] need = new int[128];  
    int[] have = new int[128];  
    for (int i = 0; i < t.length(); i++) {  
        // int 类型默认值是0,不需要初始化  
        need[t.charAt(i)]++;  
    }  
    //t中的字符在s中出现的频次,这是一个很重要的概念,通过这个概念,可以直接判断滑动窗口中是否已经包含了t中所有的字符,真的是太棒了!!  
    int count = 0;  
    int scopeLeft = 0;  
    String result = "";  
    for (int scopeRight = 0; scopeRight < s.length(); scopeRight++) {  
        char charRight = s.charAt(scopeRight);  
        //无关的数据直接跳过  
        //每次操作have之前,都在need中对比一遍,可以节省大量的对比操作  
        if (need[charRight] == 0) {  
            continue;  
        }  
        have[charRight]++;  
        //这一步很精髓,count的值,实际上就是已经匹配的t中的元素的个数,  
        // charRight这个元素出现了t中出现的次数(need[charRight]),count就不再增加,这个不再增加的操作太妙了  
        if (have[charRight] <= need[charRight]) {  
            // have[charRight] 的数量可能超出 need[charRight],只是超出之后,count 不会再增加  
            count++;  
        }  
        while (count == t.length()) {  
            if (result.equals("") || scopeRight - scopeLeft + 1 < result.length()) {  
                result = s.substring(scopeLeft, scopeRight + 1);  
            }  
            char charLeft = s.charAt(scopeLeft);  
            //无关的字符,直接跳过,不需要做任何比较  
            //每次操作have之前,都在need中对比一遍,可以节省大量的对比操作  
            if (need[charLeft] == 0) {  
                scopeLeft++;  
                continue;  
            }  
            //移除 s.charAt(scopeLeft) 就会破坏条件,所以count要变  
            if (have[charLeft] == need[charLeft]) {  
                // have[charLeft] 的数量是可能超出 need[charLeft]的,只有再减少到 need[charLeft] 之后,在减少才会影响条件  
                count--;  
            }  
            have[charLeft]--;  
            scopeLeft++;  
        }  
    }  
    return result;  
}

相关题

209. 长度最小的子数组
904. 水果成篮
用数组统计字符出现次数
242. 有效的字母异位词
383. 赎金信
438. 找到字符串中所有字母异位词